/*
    This file is part of Lua Invoke.
    Copyright 2005-2007 Rafael de Oliveira Jannone <rafael@jannone.org>

    Lua Invoke is free software; you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    Foobar is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/* invoke module */

#ifndef WIN32

#include <dlfcn.h>

#endif

#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <malloc.h>

#include "invoke.h"
#include "type.h"
#include "callback.h"
#include "cbslots.h"
#include "marshall.h"
#include "boot.h"

#define dump(pos)	printf("pos %d: %s\n", pos, lua_typename(L, lua_type(L, pos)));

/* module functions */

static int 
Lcallback_register(lua_State *L)
{
	const char* sign = lua_tostring(L, -1);
	callback_t* cb = callback_nextfree();
	if (!cb) {		
		lua_pushstring(L, "no available callback slots");
		lua_error(L);
		return 0;
	}
	cb->L = L;
	cb->sign = (const char*)strdup(sign);
	
	//printf("invoke module: callback_t* at %x\n", cb);
	
	void *userdata = lua_newuserdata(L, sizeof(callback_t*));
	*(callback_t**)userdata = cb;
	
	/* set callback metatable from invoke.lua */
	lua_pushstring(L, MYNAME);
	lua_gettable(L, LUA_GLOBALSINDEX);
	lua_pushstring(L, "_mt_callback");
	lua_rawget(L, -2);
	lua_remove(L, -2);
	lua_setmetatable(L, -2);
	
	lua_pushnumber(L, cb->slot);
	
	return 2;
}

static int Lcallback_unregister(lua_State *L)
{
	callback_t* cb = *(callback_t**)lua_touserdata(L, -1);
	callback_unregister(cb);
	return 0;
}

static int Lopen(lua_State *L)
{
	const char* dlname = lua_tostring(L, -1);
	void* handle = dlopen(dlname, RTLD_LAZY);
	if (!handle) {
		return 0;
	}
	void** p = (void**)lua_newuserdata(L, sizeof(handle));
	*p = handle;

	/* set library handle metatable from invoke.lua */
	lua_pushstring(L, MYNAME);
	lua_gettable(L, LUA_GLOBALSINDEX);
	lua_pushstring(L, "_mt_handle");
	lua_rawget(L, -2);
	lua_remove(L, -2);
	lua_setmetatable(L, -2);

	dlerror();

	return 1;
}

static int Lclose(lua_State *L)
{
	void **p = (void**)lua_touserdata(L, -1);
	dlclose(*p);
	return 0;
}

static int Lsym(lua_State *L) 
{
	void **p = (void**)lua_touserdata(L, -2);
	const char* symbol = lua_tostring(L, -1);
	void *func = dlsym(*p, symbol);
	const char *error = dlerror();
	if (!func && !error) {
		error = "symbol not found";
	}
	if (error) {
		lua_pushstring(L, error);
		lua_error(L);
		return 0;
	}
	lua_pushlightuserdata(L, func);
	return 1;
}

static int Lcall(lua_State *L)
{
	int n = lua_gettop(L);
	
	/* function pointer */	
	void *func = lua_touserdata(L, -n);
	
	/* function signature */
	const char* sign = lua_tostring(L, -n+1);
	const char* read = sign;
	
	/* private stack and output pointer */
	LL stack[13];
	long* out = (long*)stack;
	
	/* index of malloc'ed items which need to be free'd afterwards */
	long freeme[24];
	long* freeme_p = freeme;

	/* index of malloc'ed references */
	long refs[24];
	long* refs_p = refs;
	
	/* read return type token */
	type_t rt;
	read = type_read(&rt, read);

	/* marshall arguments to "stack" */
	type_t t;
	int c;
	for (c = 3; c<= n; c++) {
		if (!*read || out >= (long*)&stack[12])
			break;
		read = type_read(&t, read);
		if (t.array) {
			bool istable = lua_istable(L, c);
			if (!istable) {
				lua_pushstring(L, "table expected as array argument");
				lua_error(L);
				return 0; /* FIXME: error conditions should terminate cleanly (goto?) */
			}
			/* get number of elements */
			int len = luaL_getn(L, c);
						
			/* allocate temporary block */
			int asz = type_sizeof(&t) * len;
			long* block = (long*)malloc(asz);
			long* wblock = block;
			
			*freeme_p++ = (long)block;
			
			/* write values on block */
			int k;
			for (k = 1; k <= len; k++) {
				lua_pushnumber(L, k);
				lua_rawget(L, c);
				wblock = marshall(L, -1, wblock, &t);
				lua_pop(L, 1);
			}
			
			/* finally, push the block pointer on call stack */
			*out = (long)block;
			++out;
		} 
		else
		{
			if (t.ref) 
			{
				/* if type is byref, we need to allocate a temporary block
					and push only the reference to it.
					after the call, we demarshall this block back to lua 
					as supplementary return values */

				int tsz = type_sizeof(&t);
				long *block = (long*)malloc(tsz);
				*freeme_p++ = (long)block;
				
				marshall(L, c, block, &t);				
				*refs_p++ = (long)block;

				*out = (long)block;
				++out;
			}
			else 
			{
				out = marshall(L, c, out, &t);
			}
		}		
	}
	
	/* execute call */
	LL ret;
	LL *st = stack;
	ret = ((LL(*)(LL,LL,LL,LL,LL,LL,LL,LL))func)
		(st[0], st[1], st[2], st[3], st[4], st[5], st[6], st[7]);

	int nret = demarshall(L, ret, &rt);

	/* push back references */
	read = sign;
	long* refs_i = refs;
	while (refs_i != refs_p) {
		do {
			read = type_read(&t, read);
		} while (!t.ref);
		int tsz = type_sizeof(&t);

		/* we read exactly the number of allocated bytes, so we don't cross any forbidden boundaries :)
			but i admit not knowing the effect of doing otherwise so... FIXME */
		switch (tsz) {
			case 1:
				nret += demarshall(L, *(char*)*refs_i, &t);
				break;
			case 2:
				nret += demarshall(L, *(short*)*refs_i, &t);
				break;
			case 4:
				nret += demarshall(L, *(long*)*refs_i, &t);
				break;
			case 8:
				nret += demarshall(L, *(LL*)*refs_i, &t);
				break;
		}
		++refs_i;
	}

	/* free temporary blocks */
	while (freeme != freeme_p) {
		free((void*)*--freeme_p);
	}

	return nret;
}

static int Lset_gc(lua_State *L)
{
	char tname[64];	
	int n = lua_gettop(L);
	const void *gc_fn = lua_topointer(L, -n+1);

	sprintf(tname, "%x", (unsigned int)gc_fn);
	
	//printf("required metatable for function @ %s\n", tname);
	
	if (luaL_newmetatable (L, tname) == 1) {
		// metatable tname didn't yet exist, populate new one
		//printf("created metatable for function @ %s\n", tname);		
		lua_pushstring(L, "__gc");
		lua_pushvalue (L, -3);
		lua_rawset(L, -3);
	}
	
	//printf("stack type %s\n", lua_typename(L, lua_type(L, -3)));
	
	if (lua_setmetatable(L, -3) == 0) {
		lua_pushboolean(L, 0);
	} else {
		lua_pushboolean(L, 1);	
	}
	return 1;
}

static int Lmarshall(lua_State *L) {
	int n = lua_gettop(L), base = n-2;
	long* p = (long*)lua_touserdata(L, base);
	type_t t;

	lua_pushnil(L);
	while (lua_next(L, base+1) != 0) {
		// key is at index -2 and value at index -1
		if (lua_isnumber(L, -2) && lua_isstring(L, -1)) {
			const char* member = lua_tostring(L, -1);
			const char* div = strchr(member, ':');
			if (div) {
				int name_sz = (int)(div - member);
				++div;
				type_read(&t, div);
				lua_pushlstring(L, member, name_sz);				
				lua_gettable(L, base+2);
				p = marshall(L, -1, p, &t);
				lua_pop(L, 1);
			}
		}
		lua_pop(L, 1);
	}
	return 0;
}

static int Ldemarshall(lua_State *L) {
	int n = lua_gettop(L), base = n-1;
	
	unsigned char* p = (unsigned char*)lua_touserdata(L, base);
	type_t t;
	
	lua_newtable(L);

	lua_pushnil(L);
	while (lua_next(L, base+1) != 0) {
		// key is at index -2 and value at index -1
		if (lua_isnumber(L, -2) && lua_isstring(L, -1)) {
			const char* member = lua_tostring(L, -1);
			const char* div = strchr(member, ':');
			if (div) {
				int name_sz = (int)(div - member);
				LL val = 0;				
				int sz;
				++div;
				type_read(&t, div);
				lua_pushlstring(L, member, name_sz);
				lua_remove(L, -2);
				sz = type_sizeof(&t);
				memcpy(&val, p, sz);
				demarshall(L, val, &t);
				lua_rawset(L, -4);		
				p += sz;
				continue;
			}
		}
		lua_pop(L, 1);
	}
	return 1;
}

static const luaL_reg R[] =
{
	{ "open",	Lopen	},
	{ "close",	Lclose	},
	{ "sym",	Lsym	},
	{ "call",	Lcall	},
	{ "cb_register", Lcallback_register },
	{ "cb_unregister", Lcallback_unregister },
	{ "set_gc", Lset_gc },
	{ "marshall", Lmarshall },	
	{ "demarshall", Ldemarshall },	
	{ NULL,		NULL	}
};

LUAMODULE_API int luaopen_invoke(lua_State *L)
{
	callback_init_slots();
	luaL_openlib(L,MYNAME,R,0);
	lua_pushliteral(L, "version");
	lua_pushliteral(L, MYVERSION);
	lua_settable(L,-3);

	/* assert(loadstring(boot, "invoke boot"))() */
	lua_pushstring(L, "assert");
	lua_gettable(L, LUA_GLOBALSINDEX);
	lua_pushstring(L, "loadstring");
	lua_gettable(L, LUA_GLOBALSINDEX);
	lua_pushlstring(L, boot_bcodes, MAX_BOOT);
	lua_pushstring(L, "invoke boot");
	lua_call(L, 2, 1);	/* call loadstring */
	lua_call(L, 1, 1);	/* call assert */
	lua_call(L, 0, 0);	/* call chunk function */

	return 1;
}


LUAMODULE_API const char* luaLM_version()
{
	return LUA_VERSION;
}


LUAMODULE_API int luaLM_import(lua_State* L)
{
	return luaopen_invoke(L);
}
